package org.eclipse.iota.bounds;

import static org.eclipse.iota.bounds.BoundType.LOWER;
import static org.eclipse.iota.bounds.BoundType.UPPER;
import static org.eclipse.iota.model.EquationType.GREATER;
import static org.eclipse.iota.model.EquationType.LESS;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.iota.mapper.AbstractExpressionMapper;
import org.eclipse.iota.model.Clause;
import org.eclipse.iota.model.Constant;
import org.eclipse.iota.model.Constraint;
import org.eclipse.iota.model.Equation;
import org.eclipse.iota.model.Expression;
import org.eclipse.iota.model.Operation;
import org.eclipse.iota.model.Reference;
import org.eclipse.iota.model.UnaryOperation;
import org.eclipse.iota.util.IotaCopyUtil;
import org.eclipse.iota.util.IotaEqualityUtil;

/**
 * @author Christian Krause
 * @generated NOT
 */
public class IotaRangeCalculator extends AbstractExpressionMapper<Range> {
	
	// Mapped expressions.
	private Map<Expression,Range> mapped;
	
	// The current clause.
	private Clause clause = null;
	
	/**
	 * Default constructor.
	 */
	public IotaRangeCalculator() {
		super();
		mapped = new HashMap<Expression,Range>();
	}
	
	/**
	 * Set the clause to be used.
	 * @param clause Clause.
	 */
	public void setClause(Clause clause) {
		this.clause = clause;
	}
	
	/**
	 * Clear cached results.
	 */
	public void clear() {
		mapped.clear();
	}
	
	/**
	 * Static method for calculating the range for the given 
	 * target expression and clause. This creates a new instance 
	 * of this class, sets the clause and invokes {@link #map(Expression)}.
	 * @param target Target expression.
	 * @param clause Scope to be used.
	 * @return The calculated range.
	 */
	public static Range calculateRange(Expression target, Clause clause) {
		IotaRangeCalculator calculator = new IotaRangeCalculator();
		calculator.setClause(clause);
		return calculator.map(target);
	}
		
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.iota.mapper.AbstractExpressionMapper#map(org.eclipse.iota.core.Expression)
	 */
	@Override
	public Range map(Expression target) {
				
		// Check if the reference has been visited already.
		if (mapped.containsKey(target)) {
			return mapped.get(target);
		}
		
		Range result = new Range();
		mapped.put(target,result);
		
		// Calculate range and refine the result.
		result.join(super.map(target));
		result.join(findRange(target));
		return result;
		
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.iota.mapper.AbstractExpressionMapper#map(org.eclipse.iota.core.Constant)
	 */
	@Override
	protected Range map(Constant constant) {
		return new Range(new Bound(constant.copy(), LOWER), new Bound(constant.copy(), UPPER));
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.iota.mapper.AbstractExpressionMapper#map(org.eclipse.iota.core.Reference)
	 */
	@Override
	protected Range map(Reference reference) {
		return findRange(reference);
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.iota.mapper.AbstractExpressionMapper#performOperation(java.lang.Class, java.lang.Object, java.lang.Object)
	 */
	@Override
	protected Range performOperation(Class<? extends Operation> type, Range r1, Range r2) {

		// Check whether the first range is null:
		if (r1==null) {
			throw new RuntimeException("Performing an operation requires at least one argument.");
		}
		
		// Unrestricted range as initial result:
		Range result = new Range();
		
		// Check whether the second range is null:
		if (r2==null) {
			
			// Must be a unary operation then:
			if (!UnaryOperation.class.isAssignableFrom(type)) {
				throw new RuntimeException("Performing a non-unary operation requires more than one argument.");
			}

			// Compute the new range:
			for (Bound b1 : r1.getBounds()) {
				// Perform the operation and update the range.
				result.join(IotaBoundCalculator.performOperation(type, b1, null));
			}

		} else {
		
			// Compute the new range:
			for (Bound b1 : r1.getBounds()) {
				for (Bound b2 : r2.getBounds()) {
					// Combine the two bounds and update the range.
					result.join(IotaBoundCalculator.performOperation(type, b1, b2));
				}
			}
		}
		
		return result;
	}
	
	
	/*
	 * Find the range for a expression in a clause. 
	 * Returns <code>null</code> if now bound was found.
	 */
	private Range findRange(Expression expression) {
		
		// Create an unconstrained range.
		Range result = new Range();		
		for (Constraint constraint : clause.getConstraints()) {
	
			if (constraint instanceof Equation) {
				Equation equation = (Equation) constraint;
				
				// We need the expression on the left-hand side. Careful: the equation could be x=x!
				if (IotaEqualityUtil.equals(expression, equation.getRight()) && 
					!IotaEqualityUtil.equals(expression, equation.getLeft())) {
					equation = IotaCopyUtil.copy(equation);
					equation.flip();
				}
				
				// Left-hand side must be the expression.
				if (IotaEqualityUtil.equals(expression, equation.getLeft())) {
					
					// Calculate the new range:
					Range range = map(equation.getRight());
					
					// Make sure the result make sense:
					if (equation.getType()==GREATER) {
						range.setUpperBound(null);
						if (range.getLowerBound()!=null) {
							range.getLowerBound().getValue().increase();
						}
					}
					if (equation.getType()==LESS) {
						range.setLowerBound(null);
						if (range.getUpperBound()!=null) {
							range.getUpperBound().getValue().decrease();
						}
					}
					
					// Update the resulting range:
					result.join(range);
					
				}
				
			}
		}
		
		// Done.
		return result;
		
	}
	
}
